/*
 * Copyright (C) 2013 Daniel Wasilew <daniel.wasilew@codeminer.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.codeminer.parts.specification.source;

import de.wusel.picotask.xml.element.XMLElement;
import org.codeminer.parts.specification.CreationException;
import org.codeminer.parts.specification.DocCreator;
import org.codeminer.parts.specification.ParserFileLocations;
import org.codeminer.parts.specification.general.EmptyLinePart;
import org.codeminer.parts.specification.general.FilePart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

/**
 *
 * @author Daniel Wasilew <daniel.wasilew@codeminer.org>
 */
public class JavaFileCreator implements DocCreator {

    private static final Logger log = LoggerFactory.getLogger(JavaFileCreator.class);
    private static final String SPACE = "    ";
    private final List<FilePart> fileParts = new LinkedList<>();
    private final ParserFileLocations locations;

    public JavaFileCreator(ParserFileLocations locations) {
        fileParts.add(new PackagePart());
        fileParts.add(new EmptyLinePart());
        fileParts.add(new CommentPart());
        fileParts.add(new EmptyLinePart());
        fileParts.add(new ImportPart());
        fileParts.add(new EmptyLinePart());
        fileParts.add(new ClassStartPart());
        fileParts.add(new EmptyLinePart());
        fileParts.add(new VersionPart());
        fileParts.add(new EmptyLinePart());
        fileParts.add(new ParameterPart());
        fileParts.add(new EmptyLinePart());
        fileParts.add(new ConstructorPart());
        fileParts.add(new EmptyLinePart());
        fileParts.add(new DefaultMethodsPart());
        fileParts.add(new EmptyLinePart());
        fileParts.add(new GetterMethodsPart());
        fileParts.add(new EmptyLinePart());
        fileParts.add(new ResponseMethodsPart());
        fileParts.add(new ClassEndPart());
        this.locations = locations;
    }

    @Override
    public void start() {
        //nothing to do here since every element will become his own java-file
    }

    @Override
    public void create(XMLElement element, File file) throws CreationException {
        if (!locations.getJavaSourceOutputDirectory().exists()) {
            log.error("outputdirectory [{}] does not exist", locations.getJavaSourceOutputDirectory().getAbsolutePath());
            throw new CreationException("output directory does not exists");
        }
        XMLElement classElement = element.getChild("class");
        String className = classElement.getAttributes().getString("name");
        String version = element.getChild("version").getValue();


        createClassIfNecessary(className, version, element);
    }

    @Override
    public void end() {
        //see start();
    }

    private void createClassIfNecessary(String className, String version, XMLElement element) throws CreationException {

        log.info("check creation of [{}.java] with version [{}]", className, version);
        String fileName = className + ".java";
        File outputFile = new File(locations.getJavaSourceOutputDirectory(), fileName);
        if (locations.alwaysRecreate()) {
            outputFile.delete();
        } else if (outputFile.exists()) {
            log.info("class file already exists, will now check version");
            String existingVersion = null;
            BufferedReader reader = null;
            try {
                reader = new BufferedReader(new FileReader(outputFile));
                String line = reader.readLine();
                while (line != null) {
                    if (line.contains("private final String VERSION")) {
                        existingVersion = line.substring(line.indexOf('"') + 1, line.lastIndexOf('"'));
                    }
                    line = reader.readLine();
                }
            } catch (IOException ex) {
                throw new CreationException(ex);
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException ignored) {
                        System.out.println();
                    }
                }
            }

            assert existingVersion != null;
            if (existingVersion.equals(version)) {
                log.info("nothing to do since versions are equals");
                return;
            } else {
                log.info("will delete existing java file");
                outputFile.delete();
                try {
                    outputFile.createNewFile();
                } catch (IOException ex) {
                    throw new CreationException(ex);
                }
            }
        }

        try (PrintWriter writer = new PrintWriter(outputFile)) {
            for (FilePart filePart : fileParts) {
                filePart.writeTo(writer, element);
            }
        } catch (FileNotFoundException ex) {
            throw new CreationException(ex);
        }

    }

    private static final class ImportPart implements FilePart {

        @Override
        public void writeTo(PrintWriter writer, XMLElement parsedElement) {
            writer.println("import java.util.Set;");
            writer.println("import java.util.EnumSet;");
            writer.println();
            writer.println("import org.codeminer.parts.server.permissions.Permission;");

            XMLElement importsElement = parsedElement.getChild("imports");
            if (importsElement != null) {
                writer.println();
                List<XMLElement> children = importsElement.getChildren("import");
                for (XMLElement xMLElement : children) {
                    writer.println(xMLElement.getValue());
                }
            }
        }
    }

    private static final class ClassStartPart implements FilePart {

        @Override
        public void writeTo(PrintWriter writer, XMLElement parsedElement) {
            writer.println("public class " + parsedElement.getChild("class").getAttributes().getString("name") + " implements Action {");
        }
    }

    private static final class VersionPart implements FilePart {

        @Override
        public void writeTo(PrintWriter writer, XMLElement parsedElement) {
            writer.println(SPACE + "private final String VERSION = \"" + parsedElement.getChild("version").getValue() + "\";");
        }
    }

    private static final class ParameterPart implements FilePart {

        @Override
        public void writeTo(PrintWriter writer, XMLElement parsedElement) {
            writer.println(SPACE + "private ActionContext context;");
            writer.println();
            writer.println(SPACE + "private final Set<Permission> permissions = EnumSet.noneOf(Permission.class);");
            writer.println();


            List<XMLElement> paremters = parsedElement.getChild("input").getChildren("param");
            for (XMLElement param : paremters) {
                String name = param.getAttributes().getString("name");
                String type = param.getAttributes().getString("type");
                writer.println(SPACE + "private final " + type + " " + name + ";");
            }
        }
    }

    private static final class ConstructorPart implements FilePart {

        @Override
        public void writeTo(PrintWriter writer, XMLElement parsedElement) {
            String className = parsedElement.getChild("class").getAttributes().getString("name");


            StringBuilder parameterBuilder = new StringBuilder();
            StringBuilder parameterAssignmentBuilder = new StringBuilder();
            Iterator<XMLElement> parameters = parsedElement.getChild("input").getChildren("param").iterator();
            while (parameters.hasNext()) {
                XMLElement param = parameters.next();

                String name = param.getAttributes().getString("name");
                String type = param.getAttributes().getString("type");
                parameterBuilder.append(", ");
                parameterBuilder.append(type);
                parameterBuilder.append(" ");
                parameterBuilder.append(name);

                parameterAssignmentBuilder.append(SPACE).append(SPACE).append("this.").append(name).append(" = ").append(name).append(";");
                if (parameters.hasNext()) {
                    parameterAssignmentBuilder.append("\n");
                }
            }

            StringBuilder permissionsBuilder = new StringBuilder();
            Iterator<XMLElement> permissionsElements = parsedElement.getChild("permissions").getChildren("permission").iterator();
            while (permissionsElements.hasNext()) {
                XMLElement permissionElement = permissionsElements.next();
                permissionsBuilder.append(SPACE).append(SPACE).append("this.permissions.add(Permission.").append(permissionElement.getValue()).append(");");
                if (permissionsElements.hasNext()) {
                    permissionsBuilder.append("\n");
                }
            }

            writer.println(SPACE + "public " + className + "(ActionContext context" + parameterBuilder.toString() + ") {");
            writer.println(SPACE + SPACE + "this.context = context;");
            writer.println();
            writer.println(parameterAssignmentBuilder.toString());
            writer.println();
            writer.print(permissionsBuilder.toString());
            if (permissionsBuilder.length() != 0) {
                writer.println();
            }
            writer.println(SPACE + "}");
        }
    }

    private static final class DefaultMethodsPart implements FilePart {

        @Override
        public void writeTo(PrintWriter writer, XMLElement parsedElement) {
            writer.println(SPACE + "@Override");
            writer.println(SPACE + "public String getVersion() {");
            writer.println(SPACE + SPACE + "return this.VERSION;");
            writer.println(SPACE + "}");
            writer.println();

            writer.println(SPACE + "@Override");
            writer.println(SPACE + "public Set<Permission> requiredPermissions() {");
            writer.println(SPACE + SPACE + "return this.permissions;");
            writer.println(SPACE + "}");
            writer.println();

            writer.println(SPACE + "@Override");
            writer.println(SPACE + "public Response execute() {");
            writer.println(SPACE + SPACE + "throw new UnsupportedOperationException(\"implement this\");");
            writer.println(SPACE + "}");
        }
    }

    private static final class GetterMethodsPart implements FilePart {

        @Override
        public void writeTo(PrintWriter writer, XMLElement parsedElement) {
            Iterator<XMLElement> parameters = parsedElement.getChild("input").getChildren("param").iterator();
            while (parameters.hasNext()) {
                XMLElement param = parameters.next();
                String name = param.getAttributes().getString("name");
                String type = param.getAttributes().getString("type");
                String capName = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1);
                writer.println(SPACE + "public " + type + " get" + capName + "() {");
                writer.println(SPACE + SPACE + "return this." + name + ";");
                writer.println(SPACE + "}");
                if (parameters.hasNext()) {
                    writer.println();
                }
            }
        }
    }

    private static class ResponseMethodsPart implements FilePart {

        @Override
        public void writeTo(PrintWriter writer, XMLElement parsedElement) {

            List<XMLElement> responseChildren = parsedElement.getChildren("response");
            for (XMLElement responseChild : responseChildren) {
                StringBuilder paramCallBuilder = new StringBuilder();
                StringBuilder paramAddBuilder = new StringBuilder();
                String responseName = responseChild.getChild("result").getAttributes().getString("value");
                String responseType = responseChild.getChild("type").getAttributes().getString("value");
                createParamCode(responseChild, paramCallBuilder, paramAddBuilder);
                createFixedCode(responseChild, paramAddBuilder);

                writer.println(SPACE + "private Response create" + responseName + "(" + paramCallBuilder.toString() + ") {");
                writer.println(SPACE + SPACE + "Response response = new Response(\"" + responseType + "\");");
                writer.println(paramAddBuilder.toString());
                writer.println(SPACE + SPACE + "return response;");
                writer.println(SPACE + "}\n");
            }
        }

        private void createParamCode(XMLElement responseChild, StringBuilder paramCallBuilder, StringBuilder paramAddBuilder) {
            Iterator<XMLElement> paramElements = responseChild.getChildren("param").iterator();
            while (paramElements.hasNext()) {
                XMLElement param = paramElements.next();
                String name = param.getAttributes().getString("value");
                String type = param.getAttributes().getString("type");
                String javaName;
                if (param.getAttributes().contains("javaName")) {
                    javaName = param.getAttributes().getString("javaName");
                } else {
                    javaName = name;
                }

                if (javaName.contains(".")) {
                    throw new IllegalArgumentException("attributes contains invalidChars like .");
                }

                paramCallBuilder.append(type);
                paramCallBuilder.append(" ");
                paramCallBuilder.append(javaName);

                paramAddBuilder.append(SPACE + SPACE + "response.addProperty(\"").append(name).append("\", ").append(javaName).append(");");

                if (paramElements.hasNext()) {
                    paramCallBuilder.append(", ");
                    paramAddBuilder.append("\n");
                }

            }
        }
        private void createFixedCode(XMLElement responseChild, StringBuilder paramAddBuilder) {
            Iterator<XMLElement> paramElements = responseChild.getChildren("fixed").iterator();
            while (paramElements.hasNext()) {
                XMLElement param = paramElements.next();
                String key = param.getAttributes().getString("key");
                String value = param.getAttributes().getString("value");

                paramAddBuilder.append(SPACE + SPACE + "response.addProperty(\"").append(key).append("\", ").append("\"").append(value).append("\");");

                if (paramElements.hasNext()) {
                    paramAddBuilder.append("\n");
                }

            }
        }
    }
}
